In [1]:
# uncomment following line to make an html
!jupyter nbconvert --to html CAESAR_AoA_Cal.ipynb
[NbConvertApp] Converting notebook CAESAR_AoA_Cal.ipynb to html
[NbConvertApp] Writing 51754046 bytes to CAESAR_AoA_Cal.html

Table of Contents¶

  1. Overview
  2. Radome calibration using individual speed runs
  3. Radome calibration using all level flight
  4. Does calibration depend on altitude? RF04 case study.
  5. Final calibration
  6. Radome calibration summary
  7. Does calibration depend on climb, descent?

1. Overview¶

The radome is calibrated by using an estimate of angle of attack, referred to as a reference angle of attack, $\alpha^{*}$, and fitting a relation primarily using the attack differential pressure, $\delta p_{\alpha} = p_{\alpha_{bot}} - p_{\alpha_{top}} = ADIFR$ to reproduce this reference angle of attack. Then, angle of attack is derived from $\delta p_{\alpha}$, dynamic pressure $q$, and mach number $M(p_{static}, q)$ alone.

The reference angle of attack comes from the first-order equation for $w$:

\begin{equation} w = V\sin(\alpha - \theta) + w_{p} \end{equation}

where $V$ is the true airspeed magnitude, $\alpha$ is the angle of attack, $\theta$ is the pitch angle, and $w_{p}$ is the vertical speed of the aircraft. Solving for angle of attack,

\begin{equation} \alpha = \theta + \arcsin(\frac{w-w_{p}}{V}). \end{equation}

The reference angle of attack is found using this eqaution and assuming $w = 0$:

\begin{equation} \alpha^{*} = \theta + \arcsin(\frac{w_{p}}{V}), \end{equation}

where the $\alpha^{*}$ is referred to as the reference angle of attack.

We typically entertain two ways of predicting angle of attack from radome, GPS, and IRU measurements: Option 1 (one-predictor),

\begin{equation} \alpha = c_{0} + \frac{\delta p_{\alpha}}{q} c_{1}, \end{equation}

and option 2 (two predictor),

\begin{equation} \alpha = c_{0} + \frac{\delta p_{\alpha}}{q} \left(c_{1} + M c_{2}\right). \end{equation}

Here, $\delta p_{\alpha} = ADIFR$, $q = QCF$, and $M(q, p_{s})$ is mach number, and $p_{s}=PSFD$ is static pressure. The coefficients are found by substituting $\alpha^{*}$ for $\alpha$ and minimizing $\chi^{2}$ in these angle-of-attack relations.

The $w=0$ approximation is, unfortunately, not necessarily a valid one. The vertical velocity of wind can be non-zero at small scales due to turbulence ($O(1-10)$ m/s), across the mesoscales due to gravity waves or convective motions ($O(1-10)$ m/s), and at synoptic scales due to large-scale ascent, descent ($O(.1)$ m/s).

Turbulence and waves should not prevent accurate calibration of angle-of-attack. Turbulence would add random, unbiased error to the reference angle of attack, $\alpha^{*}$. Given enough data spanning the ranges of angle of attack, the underlying $\alpha(\delta p_{\alpha}, q, p_{s})$ relation can still be fit. This should be true for GWs as well, as long as all phases are sampled (e.g. 2nd half of FF03 in CAESAR). Out of caution, however, if GWs are seen in the calibration data, these segments have been removed.

Synoptic-scale motions, however, will add a bias. If the aircraft was calibrated on a single flight leg where $w=0.1$ m/s over the whole maneuver, this would introduce an angle-of-attack bias of $\approx 0.05$ degrees and a $w$ bias of -0.1 m/s on that leg, as $w$ was assumed zero. Other legs where $w$ actually is zero would also display this bias. The strategy employed here to get an absolute calibration was to use as many maneuvers as possible, on as many flights as possible. As the mean $w$ is assumed zero, using as many maneuvers from different altitudes, geographic regions, and flights as possible will make this more likely to be true.

NOTES:

  • All plots are interactive. Full flights are initialy plotted, but the Bokeh tools on the upper-right of each figure can be used to zoom in various ways to the regions, times of interest
In [5]:
import importlib
import qc_utils
import math
import numpy as np
import netCDF4
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm, Normalize, ListedColormap
from metpy.plots import SkewT
from metpy.units import pandas_dataframe_to_unit_arrays, units
from datetime import datetime, timedelta
from IPython.display import display
from bokeh.io import push_notebook, show, output_notebook
from bokeh.layouts import row
from bokeh.layouts import gridplot
from bokeh.plotting import figure, show
from bokeh.models import Title, CustomJS, Select, TextInput, Button, LinearAxis, Range1d
from bokeh.models.formatters import DatetimeTickFormatter
from bokeh.models.tickers import DatetimeTicker
from bokeh.palettes import Category10
import warnings
import itertools 
import holoviews as hv 
from holoviews import dim, opts
import hvplot.pandas
import copy
hv.extension('bokeh', 'matplotlib')
warnings.filterwarnings('ignore')
output_notebook()
%matplotlib inline
Loading BokehJS ...

2. Maneuvers Found: NONE¶

  • No calibration maneuvers were found!
  • For sideslip, will use coefficients from the previous field campaign, ACCLIP
  • For angle-of-attack, will provide only one set of coefficients as the GV only flew at FL420 (tf01, rf02, rf03) and FL450 (rf01)
  • To get AoA, all we have is the natural burn-off of fuel to alter AoA over up to ~8 hours of flight time (rf02 and rf08)
    • Will use all the data above FL400

3. Radome calibration using all level flight¶

  • Here, all straight and level data, not just hand-curated speed runs, will be accumulated and calibrated against
In [6]:
# Open all netcdf files and read them into data frames
importlib.reload(qc_utils)

yr = '2024'
data_dir = '/Users/ckruse/qaqc/MAIRE24/v0'
project = 'MAIRE24'

# open all NetCDF files
nc_dict = qc_utils.open_nc(data_dir) # Dictionary of flight NetCDFs

# read the variables selected in caesar_utils.py from each NetCDF file
data_1hz = {} # dictionary of DataFrame's
for flight, nc in nc_dict.items():
    data_1hz[flight] = qc_utils.read_nc(nc)
    print(f"Done reading {flight}")
Found 0 ferry flights, 1 test flights, and 3 research flights
Opening all flight NetCDF Files
This file contains PRELIMINARY DATA that are NOT to be used for critical analysis.
NIDAS version: v1.2.3-60
NIMBUS version: v5.0-211
Processing Date & Time: 2024-07-18T22:24:21 +0000
Done reading rf01
Done reading rf02
Done reading rf03
Done reading tf01

Scatter plots for all flights¶

In [7]:
importlib.reload(qc_utils)
flight = 'tf01'
level_mask = qc_utils.mask_cal_data(data_1hz[flight])
qc_utils.plot_track(data_1hz[flight], title=flight)
In [8]:
importlib.reload(qc_utils)
fl_obj = qc_utils.aoa_fit(data_1hz[flight], level_mask, flight)
qc_utils.plot_maneuv_for_aoa(fl_obj)
In [9]:
importlib.reload(qc_utils)
flight = 'rf01'
level_mask = qc_utils.mask_cal_data(data_1hz[flight])
qc_utils.plot_track(data_1hz[flight], title=flight)
In [10]:
importlib.reload(qc_utils)
fl_obj = qc_utils.aoa_fit(data_1hz[flight], level_mask, flight)
qc_utils.plot_maneuv_for_aoa(fl_obj)
In [11]:
importlib.reload(qc_utils)
flight = 'rf02'
level_mask = qc_utils.mask_cal_data(data_1hz[flight])
qc_utils.plot_track(data_1hz[flight], title=flight)
In [12]:
importlib.reload(qc_utils)
fl_obj = qc_utils.aoa_fit(data_1hz[flight], level_mask, flight)
qc_utils.plot_maneuv_for_aoa(fl_obj)
In [13]:
importlib.reload(qc_utils)
flight = 'rf03'
level_mask = qc_utils.mask_cal_data(data_1hz[flight])
qc_utils.plot_track(data_1hz[flight], title=flight)
In [14]:
importlib.reload(qc_utils)
fl_obj = qc_utils.aoa_fit(data_1hz[flight], level_mask, flight)
qc_utils.plot_maneuv_for_aoa(fl_obj)
In [15]:
importlib.reload(qc_utils)

# iterate through all flights, mask out maneuvers, mask out icing, collect all flight objects
fl_objs = []
for flight, df in data_1hz.items():
    # require |roll| < 1 deg, tas > 100 m/s, |aircraft vert spd| < 8 m/s
    level_mask = qc_utils.mask_cal_data(df)
    fl_objs.append(qc_utils.aoa_fit(df, level_mask, flight))
    
qc_utils.plot_all_scatters(fl_objs)
In [16]:
importlib.reload(qc_utils)
# print coefficients for all flights
for fl_obj in fl_objs:
    fl_obj.print_coefs()
Flight: rf01, Leg: None: mach coefs:   4.8764;  12.8068;   9.3326, simple coefs:   4.8937;  20.3871
Flight: rf02, Leg: None: mach coefs:   4.9527;   3.8824;  21.6007, simple coefs:   5.0863;  22.2666
Flight: rf03, Leg: None: mach coefs:   4.7836;   0.7791;  23.5196, simple coefs:   4.8914;  20.4994
Flight: tf01, Leg: None: mach coefs:   6.1952; -17.7907;  61.9118, simple coefs:   6.4882;  33.5908

Notes¶

  • For all three research flights, the fit AoA models can pretty well predict reference AoA as fuel is burned off
    • small defiations on rf03 might be due to actual, nonzero w
  • The fit models do not do well on tf01. This flight was in the lee of the rockies (so was rf01), but perhaps on a day with actual wave activity. So, will not use tf01 in AoA calibration
In [17]:
# Get coefs with all data together, minus individual problem flights
importlib.reload(qc_utils)
skip = ['tf01']
all_flights = copy.deepcopy(fl_objs[0])
for obj in fl_objs[1:]:
    if obj.flight in skip:
        print(f"Omitting {obj.flight} from calibration")
        continue
    all_flights = all_flights.append(obj)

print("Potential set of final coefficients:")
all_flights.print_coefs()
Omitting tf01 from calibration
Potential set of final coefficients:
Flight: appended, mach coefs:   4.8757;   2.3667;  22.5787, simple coefs:   4.9849;  21.3404
In [20]:
# Organize coefficients
init_akrd_coefs = [4.6049, 18.4376, 6.7646]
init_sslip_coefs = [-0.05288, 21.155]

acclip_sslip_coefs = [-0.05288, 21.155]
#acclip akrd coefs are a function of height...
maire24_akrd_coefs = all_flights.coefs_three
In [21]:
importlib.reload(qc_utils)
qc_utils.plot_aoa_scatter_for_cal(all_flights,aoa_range=(1.5,3.75))
In [22]:
# Does the fit with mach term improve fit?
importlib.reload(qc_utils)

# mae = mean absolute error
mae_aristo = np.mean(np.abs(all_flights.akrd - all_flights.aoa_ref))
mae_two_coef = np.mean(np.abs(all_flights.akrd_two - all_flights.aoa_ref))
mae_three_coef = np.mean(np.abs(all_flights.akrd_three - all_flights.aoa_ref))
bias_aristo = np.mean(all_flights.akrd - all_flights.aoa_ref)
bias_two_coef = np.mean(all_flights.akrd_two - all_flights.aoa_ref)
bias_three_coef = np.mean(all_flights.akrd_three - all_flights.aoa_ref)

qc_utils.plot_aoa_aoa(all_flights,aoa_range=(1.5,3.75))
print(f"Mean Absolute Error Default:     {mae_aristo:.6f} deg")
print(f"Mean Absolute Error Two Coef:   {mae_two_coef:.6f} deg")
print(f"Mean Absolute Error Three Coef: {mae_three_coef:.6f} deg")
print("")
print(f"Mean Error Default:     {bias_aristo:.6f} deg")
print(f"Mean Error Two Coef:   {bias_two_coef:.6f} deg")
print(f"Mean Error Three Coef: {bias_three_coef:.6f} deg")
Mean Absolute Error Default:     0.649013 deg
Mean Absolute Error Two Coef:   0.058652 deg
Mean Absolute Error Three Coef: 0.055602 deg

Mean Error Default:     -0.649013 deg
Mean Error Two Coef:   -0.000000 deg
Mean Error Three Coef: 0.000000 deg
In [ ]: